#define LOG_TAG "mcast"
#include "log.h"

#include <stdlib.h>
#include <string.h>
#include "mcast_socket.h"
#include "sockutil.h"

#if defined(_WIN32) || defined(_WIN64)
#include <IPHlpApi.h>
#include <WS2tcpip.h>
#include <Windows.h>
#include <Winsock2.h>
#elif defined(OS_LINUX)
#include "net/if.h"
#endif

#define LOG_CHECK_SOCKET(condition, string, action)                   \
  do {                                                                \
    if (condition) {                                                  \
      log_error(LOG_TAG, string ": %s", strerror(socket_geterror())); \
      action;                                                         \
    }                                                                 \
  } while (0)

typedef struct {
  socket_t socket;
  short port;
  char ip[SOCKET_ADDRLEN];
} mcast_t;

void* mcast_socket_create(void) {
  mcast_t* mcast = NULL;
  int ret        = 0;
  int loop       = 0;
  int ttl        = 10;

  mcast = (mcast_t*)malloc(sizeof(mcast_t));
  LOG_CHECK(mcast == NULL, return NULL);
  memset(mcast, 0, sizeof(mcast_t));
  mcast->socket = socket_udp();
  LOG_CHECK(mcast->socket == socket_invalid, free(mcast); return NULL);
  ret = setsockopt(mcast->socket, IPPROTO_IP, IP_MULTICAST_LOOP, (const char*)&loop, sizeof(loop));
  LOG_CHECK_SOCKET(ret != 0, "multicast loop", socket_close(mcast->socket); free(mcast);
                   return NULL);
  ret = setsockopt(mcast->socket, IPPROTO_IP, IP_MULTICAST_TTL, (const char*)&ttl, sizeof(ttl));
  LOG_CHECK_SOCKET(ret != 0, "multicast ttl", socket_close(mcast->socket); free(mcast);
                   return NULL);
  return mcast;
}

int mcast_socket_bind_ifr(void* handle, char* ifr) {
  mcast_t* mcast             = handle;
  struct sockaddr_in addr_in = {0};

  LOG_CHECK(handle == NULL || mcast->socket == socket_invalid, return -1);
  LOG_CHECK(ifr == NULL, return -1);

  addr_in.sin_family = AF_INET;
  addr_in.sin_port   = htons(0);

#ifdef OS_WINDOWS
  // IPPROTO_IP, IP_MULTICAST_IF windows上使用该方式设置网卡ip无效
  UINT i = 0;
  ULONG ulOutBufLen;
  DWORD dwRetVal = 0;
  PIP_ADAPTER_UNICAST_ADDRESS_LH addr;
  PIP_ADAPTER_ADDRESSES pAdapter, pAdapterInfo;
  char ip[65]           = {0};
  char description[128] = {0};

  // Make an initial call to GetAdaptersInfo to get
  // the necessary size into the ulOutBufLen variable
  ulOutBufLen  = sizeof(IP_ADAPTER_ADDRESSES);
  pAdapterInfo = (PIP_ADAPTER_ADDRESSES)malloc(ulOutBufLen);
  if (NULL == pAdapterInfo) return -1;
  if (GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_GATEWAYS, NULL, pAdapterInfo,
                           &ulOutBufLen) == ERROR_BUFFER_OVERFLOW) {
    free(pAdapterInfo);
    pAdapterInfo = (PIP_ADAPTER_ADDRESSES)malloc(ulOutBufLen);
    if (NULL == pAdapterInfo) return -1;
  }

  if ((dwRetVal = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_GATEWAYS, NULL, pAdapterInfo,
                                       &ulOutBufLen)) == ERROR_SUCCESS) {
    for (pAdapter = pAdapterInfo; pAdapter; pAdapter = pAdapter->Next) {
      if (IfOperStatusUp != pAdapter->OperStatus) continue;
      if (IF_TYPE_ETHERNET_CSMACD != pAdapter->IfType && IF_TYPE_IEEE80211 != pAdapter->IfType)
        continue;

      memset(description, 0, sizeof(description));
      WideCharToMultiByte(CP_UTF8, 0, pAdapter->Description, -1, description,
                          sizeof(description) - 1, NULL, NULL);

      for (addr = pAdapter->FirstUnicastAddress; addr; addr = addr->Next) {
        if (AF_INET == addr->Address.lpSockaddr->sa_family &&
            strcmp(description[0] ? description : pAdapter->AdapterName, ifr) == 0) {
          inet_ntop(AF_INET, &((struct sockaddr_in*)addr->Address.lpSockaddr)->sin_addr, ip,
                    sizeof(ip));
          break;
          // } else if (AF_INET6 == addr->Address.lpSockaddr->sa_family) {
          //     inet_ntop(AF_INET6, &((struct sockaddr_in6 *)addr->Address.lpSockaddr)->sin6_addr,
          //     ip, sizeof(ip));
        } else {
          continue;
        }
      }
    }
  }

  free(pAdapterInfo);
  addr_in.sin_addr.s_addr = inet_addr(ip);
#elif defined(OS_LINUX)
  struct ifreq tmp_ifr = {0};

  socket_t socket = socket_tcp();
  LOG_CHECK(socket == socket_invalid, return -1);
  snprintf(tmp_ifr.ifr_name, IFNAMSIZ, "%s", ifr);
  LOG_CHECK(ioctl(socket, SIOCGIFADDR, &tmp_ifr) == -1, socket_close(socket); return -1);
  socket_close(socket);

  struct sockaddr_in* ifa = (struct sockaddr_in*)&(tmp_ifr.ifr_addr);
  addr_in.sin_addr.s_addr = inet_addr(inet_ntoa(ifa->sin_addr));
#else
  return -1;
#endif
  return socket_bind(mcast->socket, (struct sockaddr*)&addr_in, sizeof(addr_in));
}

int mcast_socket_bind(void* handle, char* ip, int port) {
  int ret        = 0;
  mcast_t* mcast = handle;

  LOG_CHECK(handle == NULL || mcast->socket == socket_invalid, return -1);
  LOG_CHECK(ip == NULL || ip[0] == '\0' || port == 0, return -1);

  snprintf(mcast->ip, sizeof(mcast->ip), "%s", ip);
  mcast->port = port;
  ret         = socket_setreuseaddr(mcast->socket, 1);
  LOG_CHECK_SOCKET(ret != 0, "reuse addr", return -1);
  ret = socket_bind_any_ipv4(mcast->socket, port);
  LOG_CHECK_SOCKET(ret != 0, "bind any addr", return -1);

  return 0;
}

int mcast_socket_rejoin(void* handle) {
  int ret        = 0;
  mcast_t* mcast = handle;
  char local[SOCKET_ADDRLEN];

  LOG_CHECK(handle == NULL || mcast->socket == socket_invalid, return -1);
  if (mcast->port != 0 && mcast->ip[0] != '\0') {
    ret = socket_getname(mcast->socket, local, NULL);
    LOG_CHECK_SOCKET(ret != 0, "getname", return -1);
    socket_multicast_leave(mcast->socket, mcast->ip, local);
    ret = socket_multicast_join(mcast->socket, (const char*)mcast->ip, local);
    LOG_CHECK_SOCKET(ret != 0, "multicast join", return -1);
  }

  return 0;
}

int mcast_socket_destory(void* handle) {
  mcast_t* mcast = handle;
  char local[SOCKET_ADDRLEN];

  LOG_CHECK(handle == NULL, return -1);

  if (mcast->port != 0 && mcast->ip[0] != '\0') {
    socket_getname(mcast->socket, local, NULL);
    socket_multicast_leave(mcast->socket, mcast->ip, local);
  }
  socket_close(mcast->socket);
  free(handle);
  return 0;
}

int mcast_socket_recv(void* handle, void* buf, int len, char* from_ip, int* from_port,
                      int timeout) {
  int ret                          = 0;
  mcast_t* mcast                   = handle;
  struct sockaddr_storage fromaddr = {};
  socklen_t addrlen                = sizeof(fromaddr);
  u_short port                     = 0;
  LOG_CHECK(handle == NULL, return -1);

  ret = socket_recvfrom_by_time(mcast->socket, buf, len, 0, (struct sockaddr*)&fromaddr, &addrlen,
                                timeout);
  if (ret == SOCKET_TIMEDOUT)
    return 0;
  else if (ret <= 0)
    return -1;
  if (from_ip && from_port) {
    LOG_CHECK(socket_addr_to((const struct sockaddr*)&fromaddr, addrlen, from_ip, &port) != 0,
              return 0);
    *from_port = (int)port;
  }
  return ret;
}

int mcast_socket_send(void* handle, void* buf, int len, char* to_ip, int to_port, int timeout) {
  int ret                        = 0;
  mcast_t* mcast                 = handle;
  struct sockaddr_storage toaddr = {};
  socklen_t addrlen              = sizeof(toaddr);
  LOG_CHECK(handle == NULL, return -1);

  ret = socket_addr_from(&toaddr, &addrlen, to_ip, to_port);
  LOG_CHECK(ret != 0, return -1);

  ret = socket_sendto_by_time(mcast->socket, buf, len, 0, (const struct sockaddr*)&toaddr, addrlen,
                              timeout);
  if (ret == SOCKET_TIMEDOUT || ret <= 0)
    return -1;
  else
    return ret;
}
